Skip to content

feat(hcg): add surface-drift check script (standards#100 §1.5)#228

Merged
hyperpolymath merged 2 commits into
mainfrom
claude/phase-e-surface-drift-check
Jun 19, 2026
Merged

feat(hcg): add surface-drift check script (standards#100 §1.5)#228
hyperpolymath merged 2 commits into
mainfrom
claude/phase-e-surface-drift-check

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Summary

Adds scripts/hcg-surface-drift-check.sh: a static, source-only audit
that asserts every wired BojRest.Router route is covered by at least
one rule in the HCG live Verb Governance Spec
(config/gateway-policy-boj.yaml).

The ADR's largest declared risk is "policy lagging the surface"
a wired route landing without a matching policy rule would default-deny
in production (an outage on a route that should be live). The live
policy header carries a manual re-verification stamp
(Re-verified 2026-05-28 against BojRest.Router) which is the only
check today that catches this; the §1.5 prerequisite checklist relies
on the same manual procedure. This script makes the check
machine-checkable without changing any policy content, runbook
procedure, or gateway code.

What the script does

  1. Extracts (verb, path-template) tuples from
    elixir/lib/boj_rest/router.ex (Plug.Router get "/…" / post "/…"
    etc.).
  2. Extracts (verb, path-pattern) tuples from
    config/gateway-policy-boj.yaml.
  3. For each wired route, concretises :name-style placeholders with a
    known probe segment and asserts at least one policy rule matches
    (literal equality for non-regex paths; ERE grep -E match against
    the concrete URL for ^… regex paths).
  4. Exit 0 on no drift, 1 on drift detected, 64 on bad usage.

Bracket-style relationship with hcg-policy-smoke.sh

  • Smoke script (hcg-policy-smoke.sh) — runs against a live
    gateway
    to confirm the policy enforces as declared (deny/allow
    paths, stealth-status canaries, default-deny no-match canary).
  • Drift check (this PR) — runs against the source files to
    confirm the policy still covers the wired surface.

Together they cover both halves of the §1.5 pre-rollout verification:
surface→policy coverage (drift) and policy→gateway enforcement (smoke).

What this PR does NOT do

Channel state note

This session could not read hyperpolymath/standards#91 / #100 (the
session's repository scope is restricted to http-capability-gateway
and boj-server), so the brief's instructed status comment on
standards#91 could not be posted. State was reconstructed from the
canonical sources in this repo (ADR-0004, the integration plan, the
audit, the rollout runbook, the live policy, and the merged-PR
commit history) plus the current main of both in-scope repos. The
analysis: Phase A/B/C/D are closed (artefacts merged, runbook §1.2 and
the Phase-D status note in the runbook header confirm); Phase E
(standards#100) is the only open phase; all remaining §1 checklist
items are owner-driven (!OWNER: placeholders, D-4 rebaseline
workflow_dispatch, cerro-torre .ctp signing, the §6.4 Trustfile flip).
This PR advances Phase E §1.5 ("Gateway-side prerequisites") by
converting one manual sub-check into an executable artefact, without
crossing into any owner-input territory.

Test plan

  • Run the script on this branch's working tree: bash scripts/hcg-surface-drift-check.sh — expect exit 0, "OK: every wired
    router route is covered by at least one policy rule." with the
    wired-count = 7 and the policy-count matching the rule total in
    config/gateway-policy-boj.yaml.
  • Run bash scripts/hcg-surface-drift-check.sh -v — expect the
    same exit 0 plus a Matched: block listing each of the seven wired
    routes against its policy rule (literal /health → literal rule;
    /cartridge/:name/invoke^/cartridge/[A-Za-z0-9_.-]+/invoke$
    regex; etc.).
  • Synthetic drift test: temporarily add a wired route to the
    router (e.g. get "/__drift_test__" do …) without a policy rule;
    re-run and expect exit 1 with the route listed under DRIFT:.
    Revert before merge.
  • Confirm shellcheck scripts/hcg-surface-drift-check.sh is clean
    (matches scripts/hcg-policy-smoke.sh's shellcheck posture).
  • Confirm SPDX header + Owner copyright match the canonical estate
    format (matches scripts/hcg-policy-smoke.sh's header shape).
  • Verify no Hypatia / governance / spdx gates fire on the new
    script file.

Refs hyperpolymath/standards#91
Refs hyperpolymath/standards#100

🤖 Generated with Claude Code


Generated by Claude Code

The HCG live policy header records a manual re-verification stamp
("Re-verified 2026-05-28 against `BojRest.Router`") confirming that
every wired BoJ HTTP route is covered by a Verb Governance Spec rule.
The ADR's largest declared risk is "policy lagging the surface" — a
new wired route landing without a matching policy rule would default-
deny in production (an outage on a route that should be live),
silently exposing the §1.5 manual-check failure-mode this stamp is
meant to gate.

This script makes the same check machine-checkable:

  - Extract `(verb, path-template)` tuples from
    `elixir/lib/boj_rest/router.ex` (Plug.Router `get "/..."` /
    `post "/..."` lines).
  - Extract `(verb, path-pattern, ...)` tuples from
    `config/gateway-policy-boj.yaml` (the live policy promoted in
    Phase E §1.5).
  - For each wired route, concretise its `:name`-style placeholders
    with a known probe segment and assert at least one policy rule
    matches (literal equality for non-regex paths; ERE `grep -E`
    against the concrete URL for `^...` regex paths).
  - Exit 0 on no drift, 1 on drift, 64 on bad usage.

Sits alongside `scripts/hcg-policy-smoke.sh`: the smoke script runs
*against a live gateway* to confirm the policy enforces as declared
(deny/allow paths, stealth-status canaries); this script runs *against
the source files* to confirm the policy still covers the wired
surface. Together they bracket the two halves of the §1.5 pre-
rollout verification — surface→policy coverage (this) and policy→
gateway enforcement (smoke).

Hardens the §1.5 pre-rollout posture without changing any policy
content, runbook procedure, or gateway code. The runbook can adopt
this script as the §1.5 surface-drift check in a follow-up PR; this
PR lands the artefact only.

Per single-lane HCG channel discipline (pattern set in
http-capability-gateway PRs #14, #22, #26, #30, #38 and boj-server
PRs #168, #173, #224, #226): joint-close is owner-only. This commit
refs but does not close standards#100.

Refs hyperpolymath/standards#91
Refs hyperpolymath/standards#100

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 215 issues detected

Severity Count
🔴 Critical 15
🟠 High 129
🟡 Medium 71

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Stale AI session file -- delete",
    "type": "stale",
    "file": "GEMINI.md",
    "action": "delete",
    "rule_module": "root_hygiene",
    "severity": "medium"
  },
  {
    "reason": "Issue in scorecard-enforcer.yml",
    "type": "missing_timeout_minutes",
    "file": "scorecard-enforcer.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in scorecard-enforcer.yml",
    "type": "scorecard_publish_with_run_step",
    "file": "scorecard-enforcer.yml",
    "action": "split_scorecard_publish_job",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Issue in instant-sync.yml",
    "type": "secret_action_without_presence_gate",
    "file": "instant-sync.yml",
    "action": "peter-evans/repository-dispatch",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Issue in codeql.yml",
    "type": "codeql_missing_actions_language",
    "file": "codeql.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/boj-server/boj-server/cartridges/academic-workflow-mcp/adapter/mod.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/boj-server/boj-server/cartridges/ephapax-mcp/adapter/mod.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/boj-server/boj-server/cartridges/bofig-mcp/adapter/mod.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/boj-server/boj-server/cartridges/fireflag-mcp/adapter/mod.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/boj-server/boj-server/cartridges/sanctify-mcp/adapter/mod.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@hyperpolymath hyperpolymath marked this pull request as ready for review June 19, 2026 06:32
@hyperpolymath hyperpolymath enabled auto-merge (squash) June 19, 2026 06:32
@hyperpolymath hyperpolymath disabled auto-merge June 19, 2026 06:33
@hyperpolymath hyperpolymath enabled auto-merge (squash) June 19, 2026 06:35
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 215 issues detected

Severity Count
🔴 Critical 15
🟠 High 129
🟡 Medium 71

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Stale AI session file -- delete",
    "type": "stale",
    "file": "GEMINI.md",
    "action": "delete",
    "rule_module": "root_hygiene",
    "severity": "medium"
  },
  {
    "reason": "Issue in scorecard-enforcer.yml",
    "type": "missing_timeout_minutes",
    "file": "scorecard-enforcer.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in scorecard-enforcer.yml",
    "type": "scorecard_publish_with_run_step",
    "file": "scorecard-enforcer.yml",
    "action": "split_scorecard_publish_job",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Issue in instant-sync.yml",
    "type": "secret_action_without_presence_gate",
    "file": "instant-sync.yml",
    "action": "peter-evans/repository-dispatch",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Issue in codeql.yml",
    "type": "codeql_missing_actions_language",
    "file": "codeql.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/boj-server/boj-server/cartridges/academic-workflow-mcp/adapter/mod.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/boj-server/boj-server/cartridges/ephapax-mcp/adapter/mod.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/boj-server/boj-server/cartridges/bofig-mcp/adapter/mod.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/boj-server/boj-server/cartridges/fireflag-mcp/adapter/mod.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  },
  {
    "reason": "TypeScript file detected -- banned language",
    "type": "banned_language_file",
    "file": "/home/runner/work/boj-server/boj-server/cartridges/sanctify-mcp/adapter/mod.ts",
    "action": "flag",
    "rule_module": "cicd_rules",
    "severity": "critical"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@hyperpolymath hyperpolymath disabled auto-merge June 19, 2026 06:36
@hyperpolymath hyperpolymath enabled auto-merge (rebase) June 19, 2026 06:36
@hyperpolymath hyperpolymath disabled auto-merge June 19, 2026 06:36
@hyperpolymath hyperpolymath enabled auto-merge (rebase) June 19, 2026 06:36
@hyperpolymath hyperpolymath disabled auto-merge June 19, 2026 06:47
@hyperpolymath hyperpolymath enabled auto-merge (squash) June 19, 2026 13:34
@hyperpolymath hyperpolymath disabled auto-merge June 19, 2026 13:37
@hyperpolymath hyperpolymath enabled auto-merge (rebase) June 19, 2026 13:37
@hyperpolymath hyperpolymath disabled auto-merge June 19, 2026 13:39
@hyperpolymath hyperpolymath enabled auto-merge (squash) June 19, 2026 20:58
@hyperpolymath hyperpolymath disabled auto-merge June 19, 2026 20:58
@hyperpolymath hyperpolymath enabled auto-merge (squash) June 19, 2026 20:58
@hyperpolymath hyperpolymath disabled auto-merge June 19, 2026 21:00
@hyperpolymath hyperpolymath enabled auto-merge (squash) June 19, 2026 21:00
@hyperpolymath hyperpolymath disabled auto-merge June 19, 2026 21:00
@hyperpolymath hyperpolymath enabled auto-merge (squash) June 19, 2026 21:00
@hyperpolymath hyperpolymath merged commit 64a70c5 into main Jun 19, 2026
35 checks passed
@hyperpolymath hyperpolymath deleted the claude/phase-e-surface-drift-check branch June 19, 2026 21:24
hyperpolymath added a commit that referenced this pull request Jun 20, 2026
…229)

## Summary

Wires `scripts/hcg-surface-drift-check.sh` (landed in boj-server#228,
merged 2026-06-19) into GitHub Actions, so the surface⊆policy invariant
the ADR calls its largest declared risk is re-proven on every PR rather
than relying on the manual re-verification stamp in
`config/gateway-policy-boj.yaml`'s header.

PR #228 explicitly flagged this CI wiring as the follow-up step — "a CI
wiring PR should follow [the always-trigger + changes-job] pattern. Out
of scope here." This is that follow-up; the script, the router, and the
policy are unchanged.

## What lands

A single new file: `.github/workflows/hcg-surface-drift.yml`. The
workflow follows the boj-server "always-trigger + changes-job" pattern
documented in `docs/wikis/CI-and-Required-Checks.adoc` and
`.claude/CLAUDE.md` §"CI / Required Status Checks":

- **No `on.*.paths`** — the check is always created. A path-filtered
required workflow that never fires is the failure mode that stranded
#213/#215 until #216 fixed it; this gate is built to never re-introduce
it, regardless of whether it later joins `required_status_checks`.
- **Lightweight `changes` job** recomputes relevance via `git diff
origin/<base>...HEAD` against the four paths this gate cares about —
router (`elixir/lib/boj_rest/router.ex`), live policy
(`config/gateway-policy-boj.yaml`), the drift script
(`scripts/hcg-surface-drift-check.sh`), and the workflow file itself.
Fail-safe to `run=true` on any diff failure.
- **Heavy `check` job** is `needs: changes` + `if:
needs.changes.outputs.run == 'true'`. A skipped `if:` reports SUCCESS to
any future required-context list, so unrelated PRs never pay for it and
can never be blocked by it.
- **Pinned action**, **timeout-minutes**, **concurrency group**,
**`permissions: contents: read`**, **SPDX header** — matches the
canonical pattern in `.github/workflows/abi-drift.yml`.

The `check` job invokes the script with `bash
scripts/hcg-surface-drift-check.sh -v` (matching the test plan in #228)
so it works regardless of the script's file mode — #228 committed the
script as 0644.

## What this PR does NOT do

- **Does NOT** modify the runbook §1.5 ("Gateway-side prerequisites").
Adoption of the CI gate into the §1.5 checklist is a one-line
owner-driven runbook update — the PR #228 deliberate boundary stays in
place.
- **Does NOT** add the new check to `.github/settings.yml`'s
`required_status_checks` list (currently `hypatia-scan` + `codeql`).
Promotion to required is a settings change for the owner to make once
the gate has run green on a few PRs.
- **Does NOT** modify the live policy, the example policy, the router,
the script, or any other Phase E artefact. The change is wholly within
`.github/workflows/`.
- **Does NOT** pre-empt the §6.4 Trustfile flip (`tier_2_gateway.status`
stays `PENDING`), the staging soak (§3.3), or cerro-torre `.ctp` signing
— all of which remain owner-driven per the channel doctrine reaffirmed
in #207 / #224.
- Per the single-lane HCG channel discipline (pattern set in
`http-capability-gateway` PRs #14, #22, #26, #30, #38 and `boj-server`
PRs #168, #173, #224, #226, #228): joint-close is owner-only. **This PR
refs but does not close `standards#100`.**

## Channel state note

This session could not read `hyperpolymath/standards#91` / `#100` (the
session's MCP repo scope is restricted to `http-capability-gateway` and
`boj-server`), so the brief's instructed status comment on
`standards#91` could not be posted. State was reconstructed from the
canonical sources in this repo (ADR-0004, the integration plan, the
audit, the rollout runbook, the live policy,
`docs/wikis/CI-and-Required-Checks.adoc`) plus the merged-PR history of
both in-scope repos. Analysis: Phase A/B/C/D closed; Phase E
(`standards#100`) is the only open phase; #228 (2026-06-19) is the most
recent advance and explicitly named this CI wiring as the next step.

## Test plan

- [ ] **Required**: the `changes` job runs and emits `run=true` (because
`.github/workflows/hcg-surface-drift.yml` matches the path regex), so
the `check` job is gated through, not skipped, on this PR.
- [ ] **Required**: the `check` job runs `bash
scripts/hcg-surface-drift-check.sh -v` and exits 0 with the OK message —
current `main` (64a70c5) has 7 wired routes, 28 policy rules, no drift;
locally re-verified on this branch.
- [ ] **Synthetic skip**: on a follow-up PR that touches none of the
four watched paths, `changes.outputs.run` is `false` and `check` reports
`skipped` (which counts as success for any required-context list).
- [ ] **Synthetic drift**: a temporary PR adding `get "/__drift_test__"`
to `elixir/lib/boj_rest/router.ex` without a matching policy rule fires
`run=true`, `check` exits 1 with the route listed under `DRIFT:`, and
the PR is blocked from merge if/when this gate is promoted to required.
- [ ] No `actionlint` / Hypatia / SPDX gate fires on the new workflow
file.

Refs hyperpolymath/standards#91
Refs hyperpolymath/standards#100

🤖 Generated with [Claude Code](https://claude.com/claude-code)


---
_Generated by [Claude
Code](https://claude.ai/code/session_019cKmxx6AkNjzhXT6ZoxGfx)_

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request Jun 21, 2026
## Summary

Adds `scripts/hcg-spec-coverage-check.sh`: a static, source-only audit
that asserts every HTTP route declared in
`docs/specification/openapi.yaml` is covered by at least one rule in the
HCG live Verb Governance Spec (`config/gateway-policy-boj.yaml`).

Companion / complement to PR #228's `hcg-surface-drift-check.sh`. The
two scripts bracket the contract §8 declared-surface invariant from both
directions:

| Script | Invariant | Catches |
|---|---|---|
| `hcg-surface-drift-check.sh` (#228) | wired (router.ex) ⊆ policy |
policy lag behind wiring |
| `hcg-spec-coverage-check.sh` (this PR) | declared (openapi.yaml) ⊆
policy | policy lag behind the spec |

Contract §8 (`docs/integration/http-capability-gateway-boj-contract.md`)
is explicit: "the Verb Governance Spec governs the **declared** surface
(openapi.yaml), not only the currently-wired subset.
Declared-but-unimplemented routes are still classified in the policy so
that when the gnosis handler grows them they are governed from day one
rather than silently exposed." The live policy header carries the
cross-check statement (*"Surface source:
docs/specification/openapi.yaml, cross-checked against
elixir/lib/boj_rest/router.ex"*); PR #228 made the router half
machine-checkable, this PR makes the openapi half machine-checkable.
Together they make the entire §1.5 re-verification stamp executable.

Without this check the risk is concrete: someone adds a new path to
`openapi.yaml` without a corresponding policy rule. The surface-drift
check does not catch it (the route is not yet wired in `router.ex`). The
day the route is wired, the surface-drift gate fires — but by then the
operator has to either (a) ship the wiring with a default-deny in
production for a route that should be live or (b) hold the wiring PR
until the policy catches up. Catching the gap at spec-edit time avoids
both, with no procedural cost above running the existing CI gate.

### What the script does

1. Extracts `(verb, path-template)` tuples from the `paths:` section of
`docs/specification/openapi.yaml` — path entries at exactly 2-space
indent, HTTP operations (get/post/put/delete/patch/head/options) at
exactly 4-space indent under each path. Other keys at 4-space indent
(parameters/summary/description/tags/...) are metadata, not operations,
and are skipped.
2. Extracts `(verb, path-pattern)` tuples from
`config/gateway-policy-boj.yaml` using the identical extraction block
that `hcg-surface-drift-check.sh` uses, so the two scripts cannot drift
in how they read the policy.
3. For each declared route, concretises `{name}`-style placeholders with
a known probe segment (`probe`, shared with the smoke + surface-drift
scripts so a future regex tightening fails all three in lock-step) and
asserts at least one policy rule covers it: literal equality for
non-regex paths; ERE `grep -E` match against the concrete URL for `^…`
regex paths. The declared verb must be in the policy rule's verb list.
4. Exit `0` on no gap, `1` on gap detected, `64` on bad usage.

### What this PR does NOT do

- Does **not** modify the rollout runbook §1.5 or the contract §8.
Adoption as the §1.5 declared-surface check is a separate, owner-driven
PR; this PR lands the artefact only so the runbook update is a one-line
wiring change. Matches the §228-then-runbook split.
- Does **not** wire the script into CI. Boj-server's CI discipline
(`docs/wikis/CI-and-Required-Checks.adoc` / `.claude/CLAUDE.md`)
requires path-filtered required checks to use the "always-trigger +
changes job" pattern; a CI wiring PR should follow that pattern,
matching the #228#229 split. Out of scope here.
- Does **not** modify the openapi.yaml or the policy. On this branch the
script reports OK against today's surface — every one of the 26 `(verb,
path)` pairs declared in openapi.yaml has a matching rule among the 28
`(verb, path)` rules in the live policy. The 2-rule surplus is the
policy's coverage of routes the openapi.yaml does not declare (notably
`/.well-known/boj-node-pubkey`, which the router wires but the spec does
not yet enumerate); the script intentionally does not penalise that
direction — see the script's `Limitations` header.
- Does **not** pre-empt the §6.4 Trustfile flip (`tier_2_gateway.status`
stays `PENDING`).
- Per single-lane HCG channel discipline (pattern set in
`http-capability-gateway` PRs #10, #11, #12, #14, #22, #26, #30, #38 and
`boj-server` PRs #78, #90, #106, #168, #173, #207, #208, #210, #215,
#222, #224, #226, #228, #229): joint-close is owner-only. **This PR refs
but does not close `standards#100`.**

### Channel state note

This session could not read `hyperpolymath/standards#91` / `#100` (the
session's repository scope is restricted to `http-capability-gateway`
and `boj-server`), so the brief's instructed status comment on
`standards#91` could not be posted. State was reconstructed from the
canonical sources in this repo (ADR-0004, the integration plan, the
audit, the rollout runbook, the live policy, the openapi spec, and the
merged-PR commit history) plus the current `main` of both in-scope
repos. The analysis: Phase A/B/C/D are closed (artefacts merged, runbook
§1.2 and the Phase-D status note in the runbook header confirm); Phase E
(`standards#100`) is the only open phase; all remaining §1 checklist
items are owner-driven (`!OWNER:` placeholders, D-4 rebaseline
`workflow_dispatch`, cerro-torre `.ctp` signing, the §6.4 Trustfile
flip). This PR advances Phase E §1.5 ("Gateway-side prerequisites") by
converting one half of the declared-surface invariant into an executable
artefact, mirroring exactly the script-first split of #228.

## Test plan

- [ ] Run the script on this branch's working tree: `bash
scripts/hcg-spec-coverage-check.sh` — expect exit `0`, "OK: every
openapi-declared route is covered by at least one policy rule." with
`Declared (openapi) routes: 26` and `Policy (verb,path) rules: 28`.
- [ ] Run `bash scripts/hcg-spec-coverage-check.sh -v` — expect the same
exit `0` plus a `Matched:` block listing each of the 26 declared routes
against its policy rule (literal `/health` → literal rule;
`/cartridge/{name}/invoke` → `^/cartridge/[A-Za-z0-9_.-]+/invoke$`
regex; `/grpc/{service}/{method}` → two-segment regex; `/umoja/peers`
matches both `GET` and `POST` rules; etc.).
- [ ] Synthetic gap test: build a temporary openapi.yaml containing a
single declared path with no policy rule and run `OPENAPI_FILE=... bash
scripts/hcg-spec-coverage-check.sh` — expect exit `1` with the route
listed under `GAP:`. (Verified locally on this branch.)
- [ ] Confirm `shellcheck scripts/hcg-spec-coverage-check.sh` produces
only the same `SC1001` info note that
`scripts/hcg-surface-drift-check.sh` produces today (the `\^` escape
inside a `case` pattern is intentional and matches the sibling script's
posture exactly).
- [ ] Confirm SPDX header + Owner copyright match the canonical estate
format (matches `scripts/hcg-surface-drift-check.sh`'s header shape).
- [ ] Verify `scripts/check-shebang-first.sh` is still green with the
new file present.
- [ ] Verify no Hypatia / governance / spdx gates fire on the new script
file.

Refs hyperpolymath/standards#91
Refs hyperpolymath/standards#100

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---
_Generated by [Claude
Code](https://claude.ai/code/session_013VLPKSTEMFnPYQdx6rD91b)_

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request Jun 22, 2026
…(standards#100) (#231)

## Summary

Adds `docs/integration/boj-side-observability-spec.md` — the contract
for the BoJ-side telemetry events and Prometheus metrics that the
rollout-runbook §4.2 signals require, plus coordinated edits to the
sister spec and the runbook.

The gap this closes: `gateway-observability-spec.md` §3 currently defers
all BoJ-side templates to `!OWNER: scaffolded` qualifiers, because BoJ
has zero telemetry today (`elixir/mix.exs` carries no Prometheus dep,
`BojRest.Application` mounts no exporter, `BojRest.Router` emits no
`:telemetry.execute/3` events). The rollout-runbook §3.1 success
criterion 4 ("No `X-Trust-Level` mismatches in BoJ access logs") and
rollback trigger §5.1 row 4 are consequently **unobservable via
Prometheus**.

## What lands

**New spec — `docs/integration/boj-side-observability-spec.md`**
declares:

- Four telemetry events (§1): `[:boj_rest, :router, :decision]`,
`[:boj_rest, :router, :trust_level_present]`, `[:boj_rest, :http,
:response]`, `[:boj_rest, :request, :received]`.
- Five Prometheus metric prefixes (§2) — the names
`gateway-observability-spec.md` §3 PromQL templates expect.
- Exact `BojRest.Router` instrumentation sites (§3) with `file:line`
anchors against `elixir/lib/boj_rest/router.ex`.
- The `mix.exs` dep (`:telemetry_metrics_prometheus_core ~> 1.2`),
supervisor child, and new `BojRest.Telemetry` module the wiring PR adds
(§4).
- The `/metrics` exposure policy implication (§5): the new endpoint MUST
be governed by a new `metrics-get` policy rule (`internal+stealth-404`)
— leaking the BoJ scrape externally would defeat `stealth_profiles`. The
`scripts/hcg-policy-smoke.sh` stealth canary list extends in the same
wiring PR.
- The §1.4 prerequisite this spec adds to the runbook (§6) and the
wiring PR checklist (§7).

**Sister spec edit — `gateway-observability-spec.md`:**

- §3 introduction: drop `!OWNER: scaffolded` qualifier; anchor templates
to the new sister spec.
- §3.1, §3.2, §3.3: each template's "Replace metric name with the
BoJ-side equivalent" placeholder removed; queries now name the normative
metric.
- §7 References: add the sister spec.
- Header bumped 0.1 → 0.2.

**Runbook edit — `hcg-tier2-rollout-runbook.md`:**

- §1.4: new stop-the-rollout checkbox for the BoJ-side observability
prerequisite.
- Appendix B: cross-reference the new spec.
- Header bumped 0.7 → 0.8.

## What does NOT land here

- The actual `mix.exs` / `application.ex` / `router.ex` wiring. That's a
follow-up PR following the spec's §7 checklist. Splitting the contract
from the implementation lets the spec land independently and gives the
wiring PR an unambiguous target.
- Any change to `Trustfile.a2ml` `tier_2_gateway` (still `PENDING`; flip
is the last action per runbook §6.4).
- Any change to existing telemetry on the gateway side.

## Scope of edits

| File | Change |
|---|---|
| `docs/integration/boj-side-observability-spec.md` | New (279 lines). |
| `docs/integration/gateway-observability-spec.md` | §3 templates
anchored; header bump. |
| `docs/integration/hcg-tier2-rollout-runbook.md` | §1.4 new prereq;
Appendix B; header bump. |

No code, no schemas, no policy YAML touched.

## Test plan

- [x] `bash scripts/hcg-surface-drift-check.sh` — passes (router/policy
coverage unchanged).
- [x] `bash scripts/hcg-spec-coverage-check.sh` — passes (openapi/policy
coverage unchanged).
- [ ] CI: doc-only diff; no required-gate workflow has a path filter
that would skip in a way that blocks merge (per the §"CI / Required
Status Checks" pattern documented in `.claude/CLAUDE.md`).

## Channel discipline

Phase E is multi-PR by construction (#226, #228, #229, #230, hcg#38 are
prior Phase E slices, none of which closed #100). Per runbook §6.5 (and
the channel brief) `standards#100` is joint-close-only by the owner.
This PR therefore uses `Refs` not `Closes` — same convention as the
in-flight Phase E PR set.

Refs hyperpolymath/standards#91
Refs hyperpolymath/standards#100

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---
_Generated by [Claude
Code](https://claude.ai/code/session_01QSQyd1816dhfE5azfCmKC6)_

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant